feat: createDataGrid composable#174
Open
johnleider wants to merge 47 commits into
Open
Conversation
|
commit: |
johnleider
pushed a commit
that referenced
this pull request
Apr 12, 2026
Builds on createDataTable with column layout (sizing, pinning, resizing, reordering), cell editing (commit/cancel + per-column validation), ID-based row ordering, and a row spanning map. Inherits the full table pipeline (filter/sort/paginate/select/expand/group) via spread. Refactors createDataTable to support recursive column trees: relaxes DataTableColumn.key from keyof T to string, adds children, and exposes leaves + 2D headers via new extractLeaves/computeDepth/resolveHeaders utilities. Closes #174
08d1b2b to
5fd3a60
Compare
5fd3a60 to
6630729
Compare
Wires together column layout, cell editing, row ordering, and row spanning on top of createDataTable with a ClientGridAdapter. Exports createDataGrid, createDataGridContext, and useDataGrid following the trinity pattern. Also fixes a pre-existing TypeScript error in spanning.ts (Array.from unknown type).
- Extract shared applyOrder helper, eliminating duplicate ordering logic between ClientGridAdapter and VirtualGridAdapter - Fix O(n*m) order.includes() → Set-based O(n) lookup in adapters and ordering.apply() - Simplify layout.ts columns computed to reuse pinned regions instead of double-computing resolved() - Fix editable column filter to match editing.ts guard - Remove unused RowSpanningOptions import and numbered comments - Clean up test mock imports
…ance - Fix validate(false) silently passing as valid in cell editing - Evaluate editable functions with item context via itemLookup - Pass item to validate for context-aware validation - Type-safe itemValue using KeysOfType<T, ID> instead of string - Guard onEdit callback against undefined item - Delegate ordering.apply() to applyOrder to eliminate duplication - Add same-index and bounds guards to move/reorder - Remove redundant headers from DataGridContext - Use #v0/ path aliases in grid adapters - Use isFunction type guard instead of typeof - Add missing JSDoc to exported functions and barrel files
Prefer single-word names per style convention: - customSorts → sorts, customColumnFilters → filters - openGroupKeys → opened - editableColumns → editable - itemLookup → lookup - rowDirty → entry, cellMap → cells - fromIndex/toIndex → from/to - orderedItems → ordered - splitRegions → split, regionIndex → index
- Usage section with basic grid example (search, sort, pagination, column sizing) - Adapters section covering ClientGridAdapter, ServerGridAdapter, VirtualGridAdapter - Features: column layout, cell editing, row ordering, row spanning, nested columns - Reactivity table - Pinned grid example with resize handles and pin/unpin controls - Editable grid example with inline validation and edit log - Spanning grid example with merged department cells
- Basic: project tracker with status pills, priority colors, progress bars - Pinned: financial spreadsheet with 10 columns, sticky freeze lines, resize handles - Editing: inventory editor with focus ring, inline validation, edit history - Spanning: team schedule with department spanning and availability status - Sort indicators use MDI icons instead of unicode arrows - Each example has a distinct real-world purpose that justifies grid over table
- Fix search binding (grid.search is a function, not a ref) - Fix sort toggle (ResolvedColumn has no sort() method) - Fix sort indicator (ResolvedColumn has no sorted property) - Add table-fixed class so percentage column widths are respected
Vue re-renders destroy the handle div mid-drag, losing pointer capture. Move pointermove/pointerup to document listeners that persist through re-renders. Cache table ref at drag start. Also widen handle from 1px to 8px for easier grabbing.
- useClickOutside cancels edit when clicking outside the active cell - useHotkey handles Escape globally (works even with input focused) - Both scoped via useToggleScope to only activate while editing
… columns - Pin button appears on hover, toggles between pinned left and unpinned - Resize handle hidden on last column in each pin region (no neighbor = no-op) - canResize() checks per-region position instead of global column index
…iators - Column pinning: project name pinned left with sticky positioning - Column resizing: drag handles between headers - Cell editing: click project name or budget to edit inline - Row reordering: up/down chevrons to move rows - Uses useClickOutside, useHotkey, useToggleScope, useEventListener
Categories: initialization, column layout, cell editing, row ordering, row spanning, computed access, full pipeline, adapter comparison. Tests 1K and 10K datasets per benchmark standards.
Change validate return type from `boolean | string` to `string | true` to match the FormValidationRule convention used by createForm and createValidation. Removes the dead `isString` fallback branch since non-true results are guaranteed to be strings by the type system.
- Column collection and ordering via createRegistry (replaces manual Maps) - Pin state via createGroup tri-state (selected=left, mixed=right) - Matches how createDataTable uses createGroup for sort direction - Align validate signature with v0 Rule pattern (string | true) - Sizes remain as shallowReactive Map (too hot for registry tickets)
…rison - Add search/sort pipeline benchmarks matching createDataTable names - Add 10K dataset variants for computed access - Separate "Search + sort + paginate" (comparable) from "Search + sort + paginate + layout" (grid-specific) - Use deterministic data generation in both files - Remove adapter comparison with `as any` casts
- Create one Intl.Collator per sort computation instead of per comparison (eliminates ~130K Collator instantiations for 10K item sort) - Pre-split nested key paths once outside comparator - Fast-path for simple keys (no dots) skips getNestedValue entirely - Layout resize() reads cached pinned.value instead of re-resolving
createDataTable on master renamed ServerAdapter -> ServerDataTableAdapter, ServerAdapterOptions -> ServerDataTableAdapterOptions, and dropped the separate DataTableAdapterInterface in favor of using the DataTableAdapter abstract class as both value and type. Update the rebased createDataGrid to use the new names, run lint:fix to reorder Grid module imports above Utilities, and apply the new sibling-blank-line rule to the basic example.
- layout.ts: pass reactive: true to internal createRegistry so columns/pinned refs propagate through to template watchers and v-for renders. Without reactive: true, template effects subscribed to layout.columns.value never re-ran on pin/move because the registry's values() snapshot was non-reactive (ff4d6c4 codified this convention for plugin-shaped factories exposing iteration to templates). - layout.ts: replace raw === undefined checks with isUndefined() guard from #v0/utilities, matching the project-wide sweep on master. - index.ts: add @example JSDoc blocks to createDataGrid, createDataGridContext, and useDataGrid; add @see link in the module block to surface the docs page in the rendered API reference.
Page structure now matches the peer convention used by createDataTable, createKanban, and createSortable: Usage → Architecture (new) → Adapters → Recipes (renamed from Features) → Reactivity → Examples. The new Architecture section explains how the composable layers createDataTable with grid-specific modules (layout, editing, ordering, spanning) and includes a mermaid hierarchy diagram. Examples polish: - spanning: added header bar, status summary chips, member avatars with hashed colors, centered day columns, hover row tint, footer summary - editing: added inventory stats bar (item count, total value, low stock badge), edited-cell count chip, clear-log button, editable/sortable icon hints in headers, edited-cell pulse indicator, empty-state hint - pinned: added market overview header with gainers/losers/volume, filter input + reset moved to right, three-state pin button cycle (none → left → right → none), footer with pin region breakdown
Page section order moved to: Usage → Architecture → Reactivity → Adapters → Examples → Recipes. Reactivity now sits next to Architecture so the surface inventory is visible before adapter strategy, and Recipes are after Examples so readers get full visual demos first and reach for code-only snippets when extending. All four examples (basic, pinned, editing, spanning) had no per-column minSize, so the composable's 2% default applied — verified in Playwright that dragging a resize handle could crush a neighboring column to ~18px (clear readability bug). Set minSize per column based on column content + pin-icon affordance so resizes now stop at a sensible floor and pinned columns render their full title. Also widened the pinned example's min-w from 900 to 1100px and rebalanced column sizes so the Ticker column has room for "GOOGL" plus the pin icon, and Sector (right-pinned) no longer clips on narrow viewports.
Spanning table had overflow-hidden on the container, so when the 7-column grid exceeded the docs container width the day columns got clipped instead of scrolling. Switch container to overflow-x-auto and pin the table to min-w-[720px] so the schedule is always readable end-to-end.
layout.resize() picked the next column within the same region (left, scrollable, or right), so the trailing column of any region had no neighbor and silently no-op'd. Pinned columns sitting alone in their region were effectively un-resizable. Resize now walks the columns in display order so any column except the last picks up its right-hand neighbor, including across region boundaries. In the pinned example: - canResize() drops its region-walk and uses display order to match. - The table now ships overflow:visible inline so position:sticky cells can react to scroll on [data-grid]. Without it, computed overflow on the table fell back to hidden and made the table its own scroll containing block — pinned columns scrolled away with the rest of the row. Spanning example: day-column status labels collapsed to colored dots with title tooltips so the chip width stops dominating the column.
createDataGrid spreads createDataTable's context, so it inherits the
new register / onboard / unregister / clear surface as soon as the
table refactor lands. Drop the grid's own items / itemValue options
to match — the surface now lives on the registry the table exposes.
Internals:
- DataGridContext extends DataTableContext (no second registry).
- lookup() resolves via table.get(row)?.value instead of scanning
allItems.
- Explicit get size () { return table.size } after the ...table spread
since spread snapshots the registry's size getter as a literal.
- ClientGridAdapter / VirtualGridAdapter itemKey defaults to 'id' so
callers no longer need to supply it.
All four examples (BasicGrid, PinnedGrid, EditableGrid, SpanningGrid)
and the docs page move to:
const grid = createDataGrid({ ... })
grid.onboard(rows.map(value => ({ id: value.id, value })))
The grid's own rows: { order, move, reset } namespace (post-sort row
ordering) is unchanged — that's a separate ID[] state, not row
identity.
Co-authored-by: dev-grid <noreply@anthropic.com>
Per style.md / PHILOSOPHY §3.3 single-word preference. Keeps the explicit on<Action>, is*, and can* conventions; trims everything else: - editing: formatPrice → money, totalValue → total, lowStock → low - pinned: formatVolume → volume, formatCap → cap, numericKeys → numeric, stats.gainers/losers/volume → up/down/vol, pinnedSummary → summary - spanning: dotClass → dot, avatarColor → tint, dayColumns → days - basic: progressColor → tint
…gistry Rename DataGridColumn `key` → `id` and surface column id via the layout API (`pin(id)`, `resize(id)`, `reorder` already index-based). Drop the `columns` factory option on the inner createDataTable call and instead call `table.columns.onboard(...)` so the table's column registry is populated. Updates the four examples, all tests, the docs page, and the bench.
…er subclasses Replace the custom RowOrdering primitive with a createSortable instance over row ids, synced to the table's row registry via register / unregister events so removed rows no longer leak into ordering state. Drop ClientGridAdapter and VirtualGridAdapter subclasses — the grid now uses the standard ClientDataTableAdapter / VirtualDataTableAdapter and overrides `items` with a computed that applies sortable's id sequence before slicing by pagination. BREAKING in-progress: grid.rows.move signature changes from (fromIndex, toIndex) to (id, toIndex). Index-based addressing was fragile across reactive churn; createSortable's id-based API matches createKanban.
… table.columns createColumnLayout now takes the table's columns registry as input and reads display order + leaf set from it directly. Layout-specific state (sizes, pin group) is reconciled via register/unregister/clear events so late-registered columns pick up defaults and removed columns no longer leak pin/size state. Drops the duplicated registry-handling layer. Public API of ColumnLayout (pin, resize, reorder, distribute, reset) is unchanged.
…wrapped dirty Map createCellEditing now subscribes to the row registry's unregister and clear events and clears active / error / dirty entries for rows that go away. Previously `active` could point at a phantom row and `dirty` entries leaked forever. Switches `dirty` from `ref(new Map<ID, Map>)` (deep proxy on a map of maps) to `shallowReactive(new Map())` — matches the registry collection convention and stops `cancel()` from leaving the empty per-row Map that `edit()` was pre-creating.
Switch RowSpanningOptions.columns from a frozen `readonly string[]` to `MaybeRefOrGetter<readonly string[]>` so the span map recomputes when columns are added, removed, or reordered. The grid passes `() => table.leaves.value.map(c => c.id)`, which tracks the table's reactive column registry.
b9f3d65 to
13cca90
Compare
For left/scrollable regions, `offset` is the cumulative width of preceding columns — applied as CSS `left` for sticky positioning. For the right region, the same forward accumulation made the leftmost right-pinned column stick to the right edge and overlap the columns visually to its right. Reverse the offset accumulation for the right region so `offset` always means "distance from the column's pinning edge." Consumers apply it as `right:` instead of `left:` for right-pinned columns and stacking works naturally across 2+ pinned columns. Drops the redundant example block under Usage on the docs page.
EditableGrid.vue rework:
- Active cell paints `bg-primary/10` so the edit target is unmistakable
(the outline ring alone was easy to miss against the existing hover
state).
- Replace the hand-rolled `log` array + `edits` Map with a single
`createTimeline({ size: 50, reactive: true })` instance that owns
both the history log and the undo/redo stacks.
- Add Undo / Redo buttons (and Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z bindings)
that walk the timeline and apply each entry's `from` / `to` to the
row in place.
- Drop the local `editedCells` set in favor of a computed derived from
`timeline.values()` so reverting an edit also clears its dot
indicator.
Updates the example prose on the docs page to describe the new
timeline-backed history surface.
The team-schedule example only spanned one column (department) and the status dots gave the impression that row spanning was tied to coloring, not data shape. Replace with a portfolio holdings grid that spans two columns at different granularities and uses the spanned cells to display the account and asset-class subtotals. - 11 holdings across 3 accounts and 4 asset classes - One `rowSpanning` callback resolves both `account` (top-level groupings) and `assetClass` (sub-groupings within each account) - Spanned cells double as aggregation rows by carrying the subtotal - Per-row change column with up / down / flat indicators in success / error / variant colors
PinnedGrid is the canonical 'first example' on the docs page; BasicGrid was only ever referenced by the example-under-Usage block that was dropped this session, so knip flags it as unused.
Lint rule unicorn/no-array-reverse blocked the push. toReversed is the non-mutating equivalent and clearer about intent in a computed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
createDataGrid, a headless data grid composable layered on top ofcreateDataTable. Adds column layout (sizing, pinning, resizing, reordering), cell editing with validation, row ordering, row spanning, and adapter-based virtualization.What's included
createDataGrid composable
moveandresetClientGridAdapter,ServerGridAdapter,VirtualGridAdapter; row ordering inserts between sort and paginationcreateDataGridContext,createDataGridPlugin-style helpers,useDataGridDocs
composables/data/create-data-gridwith frontmatter, intro, usage, examplesapps/docs/src/examples/composables/create-data-grid/(basic, editing, pinned, spanning)Master-alignment touch-ups (this rebase)
DataTableAdapterInterfacereferences toDataTableAdapter; updatedServerAdapter/ServerAdapterOptionsre-exports toServerDataTableAdapter/ServerDataTableAdapterOptionsafter the master adapter rename.reactive: trueto the layout registry — template-iterablepinned/columnsrefs were silently non-reactive without it (fix follows the convention codified in ff4d6c4).=== undefinedchecks withisUndefined()guard.@exampleJSDoc blocks tocreateDataGrid,createDataGridContext, anduseDataGridso they surface in<DocsApi />.CI
codecov/patchreports 88.97% on the new files —VirtualGridAdapter(16 lines) accounts for most of the gap; a follow-up will add coverage when the virtual surface lands its example.docs-checkfails — pre-existing global link-checker flakiness (npmjs.com 403s, W3C 404s), affecting every open PR.